package 下载类

import (
	"context"
	"errors"
	"fmt"
	扩展类 "gitee.com/go_888/extends/extend"
	转换类 "gitee.com/go_888/extends/goframe/gconv"
	文件类 "gitee.com/go_888/extends/goframe/gfile"
	文本类 "gitee.com/go_888/extends/goframe/gstr"
	下载类 "gitee.com/go_888/internal-call/grab/v3"
	"log"
	"net/http"
	"time"
)

type 下载任务 struct {
	父类 *下载类.Response
	错误 error
}
type X进度详情 struct {
	X进度文本   string
	X进度     float64
	X每秒速度   string
	X每秒字节   float64
	X总字节数   int64
	X已完成字节  int64
	X已持续时间  time.Duration
	X估计完成时间 time.Time
}

// 一个简单的速率限制器，每秒限制r个令牌的通过量。已发出的令牌总数由n跟踪。
type 速率限制器 struct {
	r, n int
}

func (c *速率限制器) WaitN(ctx context.Context, n int) (错误 error) {
	c.n += n
	time.Sleep(
		time.Duration(1.00 / float64(c.r) * float64(n) * float64(time.Second)))
	return
}

// X下载文件 下载指定链接的文件到指定路径或目录，并通过回调函数实时报告下载进度和速度。
// 参数:
// 下载链接: 需要下载的文件的URL,需要完整的链接,包含协议标识符 "https://", "http://"
// 路径或目录: 文件下载后保存的路径或目录,可以写成:"./123.txt","./tmp/","C:\\123.txt","E:\\SVN\\"
// 重试次数: 出错后尝试下载的次数，如果小于等于0，则默认为5次。
// 回调函数: 下载过程中的回调函数，用于更新下载进度和速度，如果不需要可以传递nil, 如果返回false则取消下载。
// 限速kb: 可选参数，限制下载速度的最大值（单位为kb）。
// 返回值:
// 如果下载过程中发生错误，则返回该错误；否则返回nil。
func X下载文件(下载链接, 路径或目录 string, 重试次数 int, 回调函数 func(进度 X进度详情) bool, 限速kb ...int) error {
	计次 := 0
	if 重试次数 <= 0 {
		重试次数 = 10
	}
	for {
		计次 = 计次 + 1
		用户取消, err := 下载文件_内部(下载链接, 路径或目录, 回调函数, 限速kb...)
		if 用户取消 {
			return err
		}
		if err != nil {
			if 重试次数 < 计次 {
				return fmt.Errorf("下载失败，重试次数达到上限: %d, 错误: %v", 计次, err)
			} else {
				扩展类.X延时(500)
				log.Printf("下载失败，重试次数: %d, 延时: %d秒, 错误: %v", 计次, 1, err)
			}
		} else {
			return nil
		}
	}
}
func 下载文件_内部(下载链接, 路径或目录 string, 回调函数 func(进度 X进度详情) bool, 限速kb ...int) (用户主动取消 bool, 错误 error) {
	任务 := X创建任务(下载链接, 路径或目录, 限速kb...)
	//go 写一个死循环, 输出进度.
	for {
		if 回调函数 != nil {
			进度 := X进度详情{
				X进度文本:   任务.X取进度(),
				X进度:     扩展类.X四舍五入(任务.父类.Progress()*100, 1),
				X每秒速度:   任务.X取每秒速度(),
				X每秒字节:   任务.父类.BytesPerSecond(),
				X总字节数:   任务.X取总字节(),
				X已完成字节:  任务.X已完成字节(),
				X已持续时间:  任务.X取已持续时长(),
				X估计完成时间: 任务.X取估计完成时间(),
			}
			用户主动取消 = !回调函数(进度)
			if 用户主动取消 == true {
				任务.X取消()
			}
		}
		if 任务.X是否已完成() {
			break
		} else {
			扩展类.X延时(500)
		}
	}
	错误 = 任务.X取错误()
	if 错误 != nil {
		return 用户主动取消, 错误
	}
	return 用户主动取消, nil
}

// X下载文件到字节集 下载指定URL的文件内容到字节集。
// 参数:
// 下载链接: 文件的URL。
// 重试次数: 出错后尝试下载的次数，如果小于等于0，则默认为5次。
// 回调函数: 下载过程中的回调函数，用于实时报告下载进度和速度，如果回调返回false，则取消下载。
// 限速kb: 可选参数，限制下载速度，单位为kb/s。
// 返回值:
// 返回字节集: 下载的文件内容。
// 错误: 如果下载失败，返回错误信息。
func X下载文件到字节集(下载链接 string, 重试次数 int, 回调函数 func(进度 X进度详情) bool, 限速kb ...int) (返回字节集 []byte, 错误 error) {
	计次 := 0
	if 重试次数 <= 0 {
		重试次数 = 10
	}
	for {
		计次 = 计次 + 1
		返回字节集, 用户取消, err := 下载文件到字节集_内部(下载链接, 回调函数, 限速kb...)
		if 用户取消 {
			return nil, err
		}
		if err != nil {
			if 重试次数 < 计次 {
				return nil, fmt.Errorf("下载失败，重试次数达到上限: %d, 错误: %v", 计次, err)
			} else {
				扩展类.X延时(500)
				log.Printf("下载失败，重试次数: %d, 延时: %d秒, 错误: %v", 计次, 1, err)
			}
		} else {
			return 返回字节集, nil
		}
	}
}
func 下载文件到字节集_内部(下载链接 string, 回调函数 func(进度 X进度详情) bool, 限速kb ...int) (返回字节集 []byte, 用户主动取消 bool, 错误 error) {
	任务 := X创建内存任务(下载链接, 限速kb...)
	//go 写一个死循环, 输出进度.
	for {

		if 回调函数 != nil {
			进度 := X进度详情{
				X进度文本:   任务.X取进度(),
				X进度:     扩展类.X四舍五入(任务.父类.Progress()*100, 1),
				X每秒速度:   任务.X取每秒速度(),
				X每秒字节:   任务.父类.BytesPerSecond(),
				X总字节数:   任务.X取总字节(),
				X已完成字节:  任务.X已完成字节(),
				X已持续时间:  任务.X取已持续时长(),
				X估计完成时间: 任务.X取估计完成时间(),
			}

			用户主动取消 = !回调函数(进度)
			if 用户主动取消 == true {
				任务.X取消()
			}
		}
		if 任务.X是否已完成() {
			break
		} else {
			扩展类.X延时(500)
		}
	}
	错误 = 任务.X取错误()
	if 错误 != nil {
		return nil, 用户主动取消, 错误
	}
	// 将下载的文件保存到更新信息中
	文件, 错误 := 任务.X取字节集()
	if 错误 != nil {
		return nil, 用户主动取消, 错误
	}
	return 文件, 用户主动取消, nil
}

// 下载链接: 需要完整的链接,包含协议标识符 "https://", "http://"
// 路径或目录: 可以写成:"./123.txt","./tmp/","C:\\123.txt","E:\\SVN\\"
// 限速kb: 可选参数, 单位kb
func X创建任务(下载链接, 路径或目录 string, 限速kb ...int) *下载任务 {
	var err1 error
	// 输入参数验证
	if 路径或目录 == "" {
		路径或目录 = "."
	}
	if 下载链接 == "" || 路径或目录 == "" {
		err1 = errors.New("下载链接和路径或目录不能为空")
	}
	if !文本类.X开头判断(下载链接, "http://") && !文本类.X开头判断(下载链接, "https://") {
		err1 = errors.New("下载链接必须以 http:// 或 https:// 开头")
	}
	if 文本类.X是否包含(路径或目录, "../") {
		err1 = errors.New("路径或目录包含非法字符")
	}

	if 文件类.X是否存在(路径或目录) == false {
		文件类.X创建文件与目录(路径或目录) //创建缺失的文件夹, grab本身自动创建有bug,文件夹不存在会报错.
	}

	Http协议头, err := http.NewRequest("GET", 下载链接, nil)
	下载参数 := 下载类.Request{
		HTTPRequest: Http协议头,
		Filename:    路径或目录,
	}
	if len(限速kb) > 0 {
		下载参数.RateLimiter = &速率限制器{r: 限速kb[0] * 1024}
	}

	if err == nil && err1 != nil {
		err = err1
	}
	return &下载任务{
		父类: 下载类.DefaultClient.Do(&下载参数),
		错误: err,
	}
}

// X创建内存任务 函数用于创建一个下载任务，该任务的下载内容仅存储在内存中，不写入本地文件系统,只能通过 X取字节集() 或 X取io读取器() 访问
// 这个函数接受一个下载链接作为参数，并可选地接受一个限速参数（以KB为单位）来限制下载速度。
// 返回值是一个指向下载任务的指针，通过这个指针可以访问下载的内容或状态。
func X创建内存任务(下载链接 string, 限速kb ...int) *下载任务 {
	var err1 error
	// 输入参数验证
	if 下载链接 == "" {
		err1 = errors.New("下载链接和路径或目录不能为空")
	}
	if !文本类.X开头判断(下载链接, "http://") && !文本类.X开头判断(下载链接, "https://") {
		err1 = errors.New("下载链接必须以 http:// 或 https:// 开头")
	}

	Http协议头, err := http.NewRequest("GET", 下载链接, nil)
	下载参数 := 下载类.Request{
		HTTPRequest: Http协议头,
		NoStore:     true, //NoStore指定下载不写入本地文件系统,下载将存储在内存中，只能通过X取字节集()或X取io读取器()访问. (原api Response.Open 或 Response.Bytes)
		Filename:    ".",  //搞不懂原因,都不写入文件了, 为什么还需要指定文件名 , 不指定就没法下载.
	}
	if len(限速kb) > 0 {
		下载参数.RateLimiter = &速率限制器{r: 限速kb[0] * 1024}
	}

	if err == nil && err1 != nil {
		err = err1
	}
	return &下载任务{
		父类: 下载类.DefaultClient.Do(&下载参数),
		错误: err,
	}
}

// 返回如果下载已完成为true。如果在下载过程中发生错误，可以通过 X取错误（） 返回该错误。
func (c *下载任务) X是否已完成() bool {
	if c.错误 != nil {
		return true //错误,直接结束任务.
	}
	return c.父类.IsComplete()
}

// 通过取消此 下载任务 的底层上下文来取消文件传输。Cancel 函数会阻塞直到传输关闭，并返回任何错误——通常为 context.Canceled。
func (c *下载任务) X取消() error {
	return c.父类.Cancel()
}

// 会阻塞调用的goroutine，直到底层文件传输完成，并返回可能发生的任何错误。如果下载已经完成，Err会立即返回。
func (c *下载任务) X取错误() error {
	if c.错误 != nil {
		return c.错误
	}
	错误 := c.父类.Err()
	// ErrBadLength表示服务器响应或现有文件的长度与预期内容长度不符。
	if 错误 == 下载类.ErrBadLength {
		return errors.New("内容长度错误")
	}
	// ErrBadChecksum 表示下载的文件未能通过校验和验证。
	if 错误 == 下载类.ErrBadChecksum {
		return errors.New("校验和不匹配")
	}
	// ErrNoFilename表示无法使用服务器的URL或响应头自动确定一个合理的文件名。
	if 错误 == 下载类.ErrNoFilename {
		return errors.New("无法确定文件名")
	}
	// ErrNoTimestamp表示无法使用远程服务器响应头自动确定时间戳。
	if 错误 == 下载类.ErrNoTimestamp {
		return errors.New("无法确定远程文件的时间戳")
	}
	// ErrFileExists 表示目标路径已经存在。
	if 错误 == 下载类.ErrFileExists {
		return errors.New("文件已存在")
	}
	if 错误 != nil && 错误.Error() == "context canceled" {
		return errors.New("下载已停止.")
	}
	return 错误
}

// 返回文件传输的大小。如果远程服务器没有指定总大小且传输不完整，返回值为 -1。
func (c *下载任务) X取总字节() int64 {
	return c.父类.Size()
}

// X取字节集 阻塞当前协程，直到底层文件传输完成，然后读取所有从已完成传输中获取的字节。如果启用了 Request.NoStore，则字节将从内存中读取。
// 如果在传输过程中发生错误，该错误将被返回。
func (c *下载任务) X取字节集() ([]byte, error) {
	if c.错误 != nil {
		return []byte{}, c.错误
	}
	return c.父类.Bytes()
}

// 返回已复制到目标的总字节数，包括从先前下载中恢复的任何字节。
func (c *下载任务) X已完成字节() int64 {
	return c.父类.BytesComplete()
}

// 返回过去五秒内的平均每秒传输KB。如果下载已完成，则返回整个下载过程中的平均字节数/秒。
func (c *下载任务) X取每秒速度() string {
	size := c.父类.BytesPerSecond()
	switch {
	case size < 1024:
		return fmt.Sprintf("%dB", int64(size))
	case size < 1024*1024:
		return fmt.Sprintf("%dK", int64(size)/1024)
	case size < 1024*1024*1024:
		return fmt.Sprintf("%.1fM", size/1024/1024)
	default:
		return fmt.Sprintf("%.1fG", size/1024/1024/1024)
	}
	return ""
}

// 返回已下载的总字节数的比例。
func (c *下载任务) X取进度() string {
	return 转换类.String(扩展类.X四舍五入(c.父类.Progress()*100, 1)) + "%"
}

// 返回文件传输的持续时间。如果传输正在进行中，持续时间将是当前时间和传输开始时间之间的差。如果传输已完成，持续时间将是完成传输过程的开始和结束时间之间的差。
func (c *下载任务) X取已持续时长() time.Duration {
	return c.父类.Duration()
}

// 返回给定当前每秒字节数（BytesPerSecond）下载预计完成的时间。如果传输已经完成，将返回实际结束时间。
func (c *下载任务) X取估计完成时间() time.Time {
	return c.父类.ETA()
}
